<?php

namespace liketp\exception;

use Throwable;
use liketp\App;
use liketp\Log;

class Handler
{

    protected $app;

    public function init(App $app)
    {
        $this->app = $app;
        ob_start();

        error_reporting(E_ALL);
        ini_set('display_errors', 0);

        set_error_handler([$this, 'errorHandle']);
        set_exception_handler([$this, 'exceptionHandle']);

        register_shutdown_function([$this, 'shutdownHandle']);
    }

    public function exceptionHandle(Throwable $error)
    {
        try {

            $this->recordLog($error);

            if ($this->app->isCli() == false) ob_clean();
            if ($this->app->isDebug()) {
                $traceData = $this->exceptionTrace($error);
                $this->outputTrace($traceData, $this->app->isCli() ? 'html' : 'beauty');
            } else {
                $this->outputMsg($error->getMessage());
            }
        } catch (Throwable $e) {
            if ($this->app->isDebug()) dump($e);
        }
    }

    public function errorHandle($code, $msg)
    {
        throw new ErrorException($msg, $code);
    }

    public function shutdownHandle()
    {
        $error = error_get_last();
        if (is_null($error)) return;

        $code = $error['type'] ?? 0;
        $code = is_numeric($code) ? $code : 0;

        $exception = new ErrorException($error['message'], $code);
        $exception->setType('ShutdownError');

        $this->exceptionHandle($exception);
    }


    //------------------------------------------------ protected
    protected function exceptionTrace($error)
    {
        $data = [
            'code' => $error->getCode(),
            'message' => $error->getMessage(),
            'file' => $error->getFile(),
            'line' => $error->getLine(),
            'trace' => $this->formatTrace($error->getTrace())
        ];

        if ($error instanceof Exception) {
            $data['type'] = $error->getType();
        }

        return $data;
    }

    protected function formatTrace(array $traces)
    {
        $traceList = [];
        foreach ($traces as $trace) {
            if (isset($trace['file']) == false) continue;
            $traceList[] = [
                'file' => $trace['file'] ?? '',
                'line' => $trace['line'] ?? '',
                'function' => $trace['function'] ?? '',
                'class' => $trace['class'] ?? '',
                'type' => $trace['type'] ?? '',
                'args' => isset($trace['args']) ? $this->flattenArgs($trace['args']) : []
            ];
        }
        return $traceList;
    }

    protected function flattenArgs($args)
    {
        $res = $args;

        if (is_array($args)) {
            $res = [];
            foreach ($args as $arg) {
                $res[] = $this->flattenArgs($arg);
            }
        } else if (is_object($args)) {
            $res = get_class($args);
        }

        return $res;
    }

    protected function recordLog($error)
    {
        $content = $error->getMessage();
        Log::record($content, 'error');
    }

    protected function outputTrace($traceData, $type = 'html')
    {
        if ($type == 'json') {
            header('Content-Type: application/json; charset=UTF-8');
            echo json_encode($traceData);
        } else if ($type == 'beauty') {
            header('Content-Type: text/html; charset=UTF-8');
            echo $this->beautyFormat($traceData);
        } else {
            dp($traceData);
        }
    }

    protected function outputMsg($msg)
    {
        $this->app->outputException($msg);
    }

    //------------------------------------------------ beauty format trace data
    protected function beautyFormat($data)
    {
        $html = '';
        $html .= '<div class="exception">';
        $html .= '<span class="code">Exception:' . ($data['code'] ?? '') . '</span>';
        if (isset($data['message'])) {
            $html .= ', <span class="message">' . $data['message']  . '</span>';
        }
        $html .= '</div>' . PHP_EOL;
        $html .= $this->beautyFormatFileLine($data['file'] ?? '', $data['line'] ?? 0);

        if (isset($data['trace'])) {
            $html .= '<div class="trace-container">';
            foreach ($data['trace'] as $k => $item) {
                $html .= $this->beautyFormatTraceItem($item, $k + 1);
            }
            $html .= '</div>' . PHP_EOL;
        }

        $cssFile = __DIR__ . DIRECTORY_SEPARATOR . 'trace_beauty.css';
        $html .= '<style>' . file_get_contents($cssFile) . '</style>';
        return $html;
    }

    protected function beautyFormatFileLine($file, $line)
    {
        if (empty($file) || empty($line)) return '';
        if (file_exists($file) == false) return '';

        $line = $line - 1;
        $lines = file($file);
        $start = max(0, $line - 5);
        $end = min(count($lines), $line + 5);
        if ($start > $end) return '';

        $pickLines = array_slice($lines, $start, $end - $start + 1);
        $content = $this->formatFileContent($pickLines, $start, $line);

        $fileEle = '<span class="file">' . $file . '</span>';
        $lineEle = '<span class="line" title="line: ' . $line . '">[' . $line . ']</span>';
        $header = '<div class="file-info">' . $fileEle . $lineEle . '</div>';
        $body = '<div class="file-content">' . PHP_EOL . $content . '</div>' . PHP_EOL;

        return $header . PHP_EOL . $body;
    }

    protected function beautyFormatTraceItem($item, $index = -1)
    {
        $html = '';

        $html .= '<div class="trace-item">';
        $html .= $index > -1 ? '<div class="line-index">' . $index . '</div>' : '';

        $html .= '<p class="function-invoke">';
        $html .= '<span class="class">' . ($item['class'] ?? '') . '</span>';
        $html .= '<span class="invoke">' . ($item['type'] ?? '') . '</span>';
        $html .= '<span class="function">' . ($item['function'] ?? '') . '</span>';
        $html .= '<span class="arguments">(' . $this->plainArgs($item['args']) . ')</span>';
        $html .= '</p>' . PHP_EOL;

        $html .= '<p class="file-line">';
        $html .= $item['file'] ?? '';
        $html .= isset($item['line']) ? ': ' . $item['line'] : '';
        $html .= '</p>' . PHP_EOL;

        $html .= '</div>' . PHP_EOL;

        return $html;
    }

    protected function formatFileContent(array $lines, int $start, int $point): string
    {
        $placeholder = '<div class="line placeholder"><span class="num"></span></div>';
        $html = $placeholder . PHP_EOL;
        foreach ($lines as $k => $v) {
            $num = $start + $k;
            $numClass = $num == $point ? 'num point' : 'num';
            $textClass = $num == $point ? 'text target-line' : 'text';
            $html .= '<div class="line">';
            $html .= '<span class="' . $numClass . '">' . $num . '</span>';
            $html .= '<pre class="' . $textClass . '">' . htmlentities($v) . '</pre>';
            $html .= '</div>';
            $html .= PHP_EOL;
        }
        $html .= $placeholder . PHP_EOL;
        return $html;
    }

    protected function plainArgs(array $args): string
    {
        $res = [];
        foreach ($args as $arg) {
            if (is_string($arg) && $arg === '') {
                $res[] = "''";
            } else if (is_bool($arg)) {
                $res[] = $arg ? 'true' : 'false';
            } else if ($arg === null) {
                $res[] = 'null';
            } else if ($arg instanceof \Closure) {
                $res[] = 'Closure';
            } else if (is_array($arg)) {
                $res[] = 'array[...]';
            } else if (is_object($arg)) {
                $res[] = 'object';
            } else {
                $res[] = (string)$arg;
            }
        }
        return implode(', ', $res);
    }
}
